""" Parser class provides base interface for futher custom parsers realization. """

import S3D.Tokenizer as tkn
from unittest import TestCase

class ParserException(Exception):
    pass

class SuppressErrorException(Exception):
    pass

class ParserData:
    def __init__(self, **kw):
        for k, v in kw.items():
            setattr(self, k, v)

class TokenizerCallback:
    def __init__(self, file = "<string>", lineOffset = 0, lineCorrection = []):
        self.file = file
        self.lineOffset = lineOffset
        self.lineCorrection = lineCorrection
        self.errorsCount = 0
        self.errorMsg = ""

    def __call__(self, msg, line, pos):
        self.errorsCount += 1

        correction = 0
        for l in self.lineCorrection:
            if line <= l:
                correction += 1

        self.errorMsg += "%s(%d): error: %s" % (self.file, line + self.lineOffset - correction, msg)

class Parser:
    def parse(self):
        pass

    def compileNested(self, parent):
        self.isNestedCompile = True
        self.errorMsg = ""
        self.errorsCount = 0
        self.file = parent.file
        self.lineOffset = parent.lineOffset

        self.tokens = parent.tokens
        self.index  = parent.index

        self.suppressError = parent.suppressError
        self.suppressionLineBackup = parent.suppressionLineBackup

        try:
            return self.parse()
        finally:
            assert self.suppressError == parent.suppressError
            
            parent.index = self.index
            parent.errorsCount += self.errorsCount
            parent.errorMsg += self.errorMsg

    def compile(self, text, file = "<string>", lineOffset = 0):
        self.isNestedCompile = False
        self.errorMsg = ""
        self.errorsCount = 0
        self.file = file
        self.lineOffset = lineOffset

        cb = TokenizerCallback(self.file, self.lineOffset)
        self.tokens = tkn.tokenize(text, cb)

        if self.tokens is None:
            self.errorMsg += cb.errorMsg
            self.errorsCount += cb.errorsCount
            return None

        self.tokens.append(tkn.Token("eof"))
        self.index  = 0
        self.suppressError = 0
        self.suppressionLineBackup = []

        try:
            r = self.parse()
            assert not r is None
            return r
        except ParserException:
            return None
        finally:
            assert self.suppressError == 0

    def pushSuppressError(self):
        self.suppressError += 1
        self.suppressionLineBackup.append(self.index)

    def popSuppressError(self):
        assert self.suppressError > 0
        
        self.index = self.suppressionLineBackup[len(self.suppressionLineBackup) - 1]
        self.suppressionLineBackup.pop()
        self.suppressError -= 1

    def getNextToken(self, offset = 0):
        return self.tokens[self.index + offset]

    def getNextValue(self, offset = 0):
        return self.tokens[self.index + offset].value

    def nextHasValue(self, *values, match = False):
        cur = self.tokens[self.index]
        if cur.value not in values:
            return False

        if match:
            self.index += 1

        return True

    def nextHasType(self, *types, offset = 0):
        cur = self.tokens[self.index + offset]
        if cur.type not in types:
            return False

        return True
    
    def nextIsToken(self, type, *value, match = False, offset = 0):
        assert not match or offset == 0

        cur = self.tokens[self.index + offset]
        if cur.type != type:
            return False

        if len(value) > 0 and cur.value not in value:
            return False

        if match:
            self.index += 1

        return True 

    def nextIsSymbol(self, *value, match = False, offset = 0):
        return self.nextIsToken('symbol', *value, match = match, offset = offset)

    def nextIsWord(self, *values, match = False):
        return self.nextIsToken('word', *values, match = match)

    def matchOperator(self, value):
        length = len(value)

        for index in range(length):
            t = self.tokens[self.index + index]

            if t.type != 'symbol' or t.value != value[index]:
                self.raiseError("'%s': unexpected token, operator '%s'" % (self.getNextValue(), value))

        self.index += length

    def nextIsOperator(self, value, match = False, checkSingle = False):
        length = len(value)

        for index in range(length):
            t = self.tokens[self.index + index]

            if t.type != 'symbol' or t.value != value[index]:
                return False

        if checkSingle and len(value) == 1 and self.nextIsSymbol(value, offset = 1):
            return False

        if match:
            self.index += length

        return True

    def tryMatchSymbol(self, value):
        return self.nextIsToken('symbol', value, match = True)

    def tryMatchOperator(self, value):
        return self.nextIsOperator(value, match = True)

    def matchAny(self):
        self.index += 1
        return self.getNextValue(-1)

    def matchNothing(self):
        cur = self.tokens[self.index]
        self.raiseError("'%s': unexpected token" % cur.value)

    def matchByValue(self, value):
        cur = self.tokens[self.index]
        if cur.value != value:
            self.raiseError("token '%s' expected" % value)

        self.index += 1

    def matchNot(self, type = None, value = None):
        cur = self.tokens[self.index]
        if not type is None and cur.type == type or not value is None and cur.value == value:
           self.raiseError("'%s': unexpected token" % cur.value)

        self.index += 1

    _VALUE_TYPES = {'int', 'float', 'string', 'word'}
    
    def matchValue(self):
        # check for +/- signs and number
        if self.nextIsSymbol('+', '-') and self.nextHasType('int', 'float', offset = 1):
            self.index += 2
            res = self.getNextValue(-2) + self.getNextValue(-1)
            if self.getNextToken(-1).type == 'int':
                return int(res)
            else:
                return float(res)

        cur = self.tokens[self.index]
        if cur.type not in Parser._VALUE_TYPES:
            self.raiseError("'%s': value expected" % cur.value)

        self.index += 1

        if cur.type == 'int':
            return int(cur.value)
        if cur.type == 'float':
            return float(cur.value)
        return cur.value

    def matchSymbol(self, *values):
        cur = self.tokens[self.index]
        if cur.type != 'symbol' or cur.value not in values:
            if len(values) == 1:
                self.raiseError("'%s': symbol '%s' expected" % (cur.value, values[0]))
            else:
                self.raiseError("'%s': symbols %s expected" % (cur.value, values))

        self.index += 1
        return cur.value

    def matchWord(self, *names):
        cur = self.tokens[self.index]

        if len(names) == 0:
            if cur.type != 'word':
                self.raiseError("'%s': identifier expected" % cur.value)
        else:
            if cur.type != 'word' or cur.value not in names:
                if len(names) == 1:
                    self.raiseError("token '%s' expected" % names[0])
                else:
                    self.raiseError("tokens %s expected" % names)

        self.index += 1
        return cur.value

    def raiseError(self, msg, line = None):
        if self.suppressError == 0:
            if line is None:
                line = self.tokens[self.index].line

            self.errorsCount += 1
            self.errorMsg += "%s(%d): error: %s" % (self.file, line + self.lineOffset, msg)
            raise ParserException()
        else:
            raise SuppressErrorException()

class Tests(TestCase):
    def testNesting(self):
        testCase = self        

        class OwnerCompiler(Parser):
            def parse(self):
                nested = NestedCompiler()

                try:
                    self.pushSuppressError()
                    nested.compileNested(self)
                    testCase.assertTrue(self.suppressError == 1)
                    self.popSuppressError()
                except SuppressErrorException:
                    testCase.assertTrue(self.suppressError == 1)
                    self.popSuppressError()

                testCase.assertTrue(self.index == 0)

                try:
                    r = nested.compileNested(self)
                    testCase.assertTrue(self.suppressError == 0)
                    testCase.assertTrue(self.index == 1)
                    return r
                except ParserException:
                    testCase.assertTrue(self.suppressError == 0)
                    testCase.assertTrue(self.index == 0)
                    raise

        class NestedCompiler(Parser):
            def parse(self):
                self.pushSuppressError()
                try:
                    self.matchSymbol('*')
                    self.popSuppressError()
                except SuppressErrorException:
                    self.popSuppressError()
                    
                return self.matchWord()

        c = OwnerCompiler()

        r = c.compile('test')
        self.assertIsNotNone(r)

        r = c.compile('10')
        self.assertIsNone(r)

        r = c.compile('*')
        self.assertIsNone(r)
